Skip to content

feat(transport): add Streamable HTTP /mcp endpoint (FT-1935)#21

Open
joalves wants to merge 8 commits into
mainfrom
feat/FT-1935/streamable-http
Open

feat(transport): add Streamable HTTP /mcp endpoint (FT-1935)#21
joalves wants to merge 8 commits into
mainfrom
feat/FT-1935/streamable-http

Conversation

@joalves
Copy link
Copy Markdown
Collaborator

@joalves joalves commented May 20, 2026

Summary

  • Adds a Streamable HTTP MCP transport at https://mcp.absmartly.com/mcp alongside the existing SSE transport at /sse. Both wrap the same MCP_OBJECT Durable Object, so every tool, resource, and prompt is exposed identically on both transports.
  • Refactors the inline /sse auth/dispatch block in src/index.ts into a reusable handleMcpTransportRequest helper so both transports share the API-key bypass + OAuth 401 flow. Pure behavior preservation for /sse clients.
  • Widens extractEndpointFromPath to accept string | readonly string[] and teaches detectApiKey to recognise endpoint segments under /mcp/<subdomain> just like /sse/<subdomain>.
  • Unlocks Gemini Enterprise's Custom MCP Server connector (Preview), which only accepts Streamable HTTP and rejects SSE. README adds a Gemini Enterprise sub-section under Option 6.
  • README also documents /mcp install for Claude Code, Cursor, Windsurf, and VS Code, leaving the SSE one-click install badges untouched for now.

Why now?

The MCP spec deprecated SSE in March 2025 in favour of Streamable HTTP. Adding /mcp is additive — no migration cost for existing /sse clients (mcp-remote, the DXT extension, Claude Desktop's connector UI).

JIRA

FT-1935

Test Plan

  • 3216 / 3216 unit tests pass locally (npm run test:unit)
  • Typecheck clean (npm run typecheck)
  • Production deploy verified (worker version c0edfd7d-8cf5-49e6-ae85-7fef5af5e711); DXT SHA matches local build
  • curl -sI https://mcp.absmartly.com/sseHTTP/2 401 with www-authenticate: Bearer realm="OAuth" (no regression)
  • curl -sI https://mcp.absmartly.com/mcpHTTP/2 401 with www-authenticate: Bearer realm="OAuth" (new)
  • wrangler dev smoke test: both /sse and /mcp 401 as expected
  • End-to-end client test deferred — requires sandbox API key. Suggested clients to try post-merge: mcp-remote --transport http https://mcp.absmartly.com/mcp ..., Cursor with "type":"http", and Gemini Enterprise's Custom MCP connector

Plan

The full implementation plan is in docs/superpowers/plans/2026-05-20-streamable-http-transport.md (local-only, gitignored). It was executed in 8 TDD-style commits via subagent-driven development.

Follow-ups (not in this PR)

From the final code review pass, captured for separate work:

  1. scripts/deploy-with-verify.sh only verifies DXT asset SHA, not worker route behaviour. Task 8 caught /mcp drifting to 404 between Task 6's deploy and Task 8's check — a redeploy fixed it. Adding a 401 + WWW-Authenticate curl assertion to the verify script (~5 lines of bash) would catch silent worker rollbacks.
  2. url.pathname.startsWith("/mcp") over-matches (e.g. would catch /mcp.json). Same issue exists for /sse. Tighten to url.pathname === "/mcp" || url.pathname.startsWith("/mcp/") for both routes.
  3. /mcp/<subdomain> path-style URLs would 405 at the handler. McpAgent.serve("/mcp", ...) uses an exact URLPattern({ pathname: "/mcp" }). detectApiKey extracts the endpoint correctly, but the transport handler itself doesn't accept the path suffix. Document this as a known limitation (clients must use /mcp with x-absmartly-endpoint header), or mount serve("/mcp/*") if path-style is needed.
  4. No automated test that the worker's fetch handler actually dispatches /mcp to streamableMcpHandler. The 3 new unit tests cover helper inputs; the dispatch wiring is currently verified only by production smoke tests. ~15-line test addition.
  5. Plan claim about apiHandlers covering /.well-known/oauth-protected-resource is inaccurate. @cloudflare/workers-oauth-provider doesn't actually serve that endpoint — only /.well-known/oauth-authorization-server. Pre-existing; not made worse by this PR. If a Streamable HTTP client (Gemini Enterprise?) reports a resource-indicator mismatch, that's the root cause.
  6. Migrate Cursor / VS Code one-click install badges from /sse/mcp with "type":"http" once the new endpoint has been stable in production for ~1 week.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added HTTP transport configuration support for multiple MCP clients (Claude Code, Cursor, Windsurf, VS Code).
    • Introduced Gemini Enterprise section with HTTP-only endpoint guidance.
  • Documentation

    • Updated README with streamable HTTP transport examples and client-specific configuration instructions.
    • Clarified Gemini Enterprise transport limitations.
  • Tests

    • Extended test coverage for endpoint extraction and API key detection across multiple transport paths.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Walkthrough

This pull request extends the MCP server with HTTP/Streamable transport support via a /mcp endpoint, complementing the existing SSE /sse route. Core changes include: a flexible endpoint detection utility accepting multiple path prefixes; a unified handleMcpTransportRequest handler that routes requests through either API-key or OAuth authentication; shared CORS configuration for both transport handlers; and integration of /mcp into the OAuth provider's router. Documentation extends to four major MCP client tools (Claude Code, Cursor, Windsurf, VS Code) with HTTP configuration examples, and introduces a new Gemini Enterprise section requiring Streamable HTTP only. Unit and integration tests validate endpoint extraction and API key detection across both /sse and /mcp paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • absmartly/mcp#1: Directly extends the same extractEndpointFromPath and detectApiKey functions in src/shared.ts to support /mcp via prefix arrays.
  • absmartly/mcp#9: Adjusts OAuth flow wiring and KV state persistence (oauth_endpoint_pending:*) for endpoint "survival" across the same /sse//mcp routing refactor.

Poem

🐰 A Tale of Two Transports

Two paths now flow through /mcp and /sse,
Where API keys and OAuth dance in harmony,
CORS blooms permissive, prefixes align—
Four clients rejoice: the config looks fine!
Gemini's enterprise nods in delight,
HTTP streaming takes graceful flight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a Streamable HTTP /mcp endpoint. It is specific, clear, and directly reflects the primary purpose of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/FT-1935/streamable-http

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/index.ts (1)

11-11: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix the missing module import before merge (Line 11).

CI is currently blocked by TS2307: Cannot find module './dxt-bundle'. Please add/restore the module (or correct the import path) so typecheck passes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` at line 11, The import for DXT_BUNDLE_BASE64 and DXT_BUNDLE_SHA
in src/index.ts cannot be resolved; restore or correct the module so TypeScript
can find it by either adding the missing module file that exports
DXT_BUNDLE_BASE64 and DXT_BUNDLE_SHA or updating the import path to the correct
location where those named exports are defined (and ensure the module actually
exports those symbols).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 446-448: The blockquote contains an undesired blank line causing
markdownlint MD028; remove the empty line between the two quoted lines so the
two `>` lines are contiguous (or convert the second quoted line into a normal
paragraph). Edit the block that currently has the two separate `>` lines (the
SSE transport note and the Reload the IDE window note) to be adjacent `>` lines
with no blank line between them so the blockquote is continuous.

In `@src/index.ts`:
- Around line 778-788: The code uses hardcoded path literals "/sse" and "/mcp"
when registering handlers and dispatching (see pathPrefix properties passed to
handleMcpTransportRequest and the url.pathname.startsWith checks); define
top-level ALL_CAPS constants (e.g., const SSE_PATH = "/sse"; const MCP_PATH =
"/mcp";) near other defaults and replace all inline occurrences with those
constants so sseMcpHandler, streamableMcpHandler, handleMcpTransportRequest and
any url.pathname.startsWith checks reference SSE_PATH and MCP_PATH instead of
string literals to prevent drift.

In `@src/shared.ts`:
- Line 106: Promote the inline array ['/sse', '/mcp'] into a top-level ALL_CAPS
constant (e.g., TRANSPORT_PREFIXES) declared with the other shared constants,
and replace the inline usage in the extractEndpointFromPath call so that
endpointFromPath is produced by extractEndpointFromPath(url.pathname,
TRANSPORT_PREFIXES); update any imports/exports if needed to keep naming
consistent.

---

Outside diff comments:
In `@src/index.ts`:
- Line 11: The import for DXT_BUNDLE_BASE64 and DXT_BUNDLE_SHA in src/index.ts
cannot be resolved; restore or correct the module so TypeScript can find it by
either adding the missing module file that exports DXT_BUNDLE_BASE64 and
DXT_BUNDLE_SHA or updating the import path to the correct location where those
named exports are defined (and ensure the module actually exports those
symbols).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f99c141-1621-43af-bfd9-fbd36f5cf9a1

📥 Commits

Reviewing files that changed from the base of the PR and between 1a60fd5 and fcbc971.

📒 Files selected for processing (5)
  • README.md
  • src/index.ts
  • src/shared.ts
  • tests/unit/shared.test.ts
  • tests/unit/streamable-routing.test.ts

Comment thread README.md
Comment on lines +446 to 448
> SSE transport (`/sse`) is **not** supported by this connector. The legacy Gemini CLI / Code Assist sections above continue to use `/sse` until those clients add Streamable HTTP support.

> Reload the IDE window after editing settings (VS Code: Command Palette → **Developer: Reload Window**). MCP support in Code Assist requires **agent preview mode** — set `"geminicodeassist.updateChannel": "Insiders"` in VS Code settings if not already enabled.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint MD028: remove the blank line inside the blockquote.

There is an empty line between two quoted lines, which triggers no-blanks-blockquote. Keep both quoted lines contiguous (or make the second one a normal paragraph).

Suggested fix
 > SSE transport (`/sse`) is **not** supported by this connector. The legacy Gemini CLI / Code Assist sections above continue to use `/sse` until those clients add Streamable HTTP support.
-
 > Reload the IDE window after editing settings (VS Code: Command Palette → **Developer: Reload Window**). MCP support in Code Assist requires **agent preview mode** — set `"geminicodeassist.updateChannel": "Insiders"` in VS Code settings if not already enabled.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> SSE transport (`/sse`) is **not** supported by this connector. The legacy Gemini CLI / Code Assist sections above continue to use `/sse` until those clients add Streamable HTTP support.
> Reload the IDE window after editing settings (VS Code: Command Palette → **Developer: Reload Window**). MCP support in Code Assist requires **agent preview mode** — set `"geminicodeassist.updateChannel": "Insiders"` in VS Code settings if not already enabled.
> SSE transport (`/sse`) is **not** supported by this connector. The legacy Gemini CLI / Code Assist sections above continue to use `/sse` until those clients add Streamable HTTP support.
> Reload the IDE window after editing settings (VS Code: Command Palette → **Developer: Reload Window**). MCP support in Code Assist requires **agent preview mode** — set `"geminicodeassist.updateChannel": "Insiders"` in VS Code settings if not already enabled.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 447-447: Blank line inside blockquote

(MD028, no-blanks-blockquote)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 446 - 448, The blockquote contains an undesired blank
line causing markdownlint MD028; remove the empty line between the two quoted
lines so the two `>` lines are contiguous (or convert the second quoted line
into a normal paragraph). Edit the block that currently has the two separate `>`
lines (the SSE transport note and the Reload the IDE window note) to be adjacent
`>` lines with no blank line between them so the blockquote is continuous.

Comment thread src/index.ts
Comment on lines +778 to +788
{ pathPrefix: "/sse", handler: sseMcpHandler },
{ apiKey, endpoint },
clientFingerprint
);
}

return await oauthProvider.fetch(request, env, ctx);
if (url.pathname.startsWith("/mcp")) {
return await handleMcpTransportRequest(
request, env, ctx,
{ pathPrefix: "/mcp", handler: streamableMcpHandler },
{ apiKey, endpoint },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace inline transport path literals with top-level constants (Line 778 and Line 787).

"/sse" and "/mcp" are hardcoded in the route objects. Move these to grouped ALL_CAPS constants and reuse them across handler registration and dispatch to avoid drift.

As per coding guidelines, "Never use magic strings or hardcoded values inline in code - all default values must be declared as constants at the top of the file".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 778 - 788, The code uses hardcoded path literals
"/sse" and "/mcp" when registering handlers and dispatching (see pathPrefix
properties passed to handleMcpTransportRequest and the url.pathname.startsWith
checks); define top-level ALL_CAPS constants (e.g., const SSE_PATH = "/sse";
const MCP_PATH = "/mcp";) near other defaults and replace all inline occurrences
with those constants so sseMcpHandler, streamableMcpHandler,
handleMcpTransportRequest and any url.pathname.startsWith checks reference
SSE_PATH and MCP_PATH instead of string literals to prevent drift.

Comment thread src/shared.ts
const authHeader = request.headers.get("Authorization");

const endpointFromPath = extractEndpointFromPath(url.pathname, '/sse');
const endpointFromPath = extractEndpointFromPath(url.pathname, ['/sse', '/mcp']);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract transport prefixes into a top-level constant (Line 106).

Using ['/sse', '/mcp'] inline introduces a magic value in a changed path. Please promote this into an ALL_CAPS constant near the other shared constants and reuse it here.

As per coding guidelines, "Never use magic strings or hardcoded values inline in code - all default values must be declared as constants at the top of the file".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared.ts` at line 106, Promote the inline array ['/sse', '/mcp'] into a
top-level ALL_CAPS constant (e.g., TRANSPORT_PREFIXES) declared with the other
shared constants, and replace the inline usage in the extractEndpointFromPath
call so that endpointFromPath is produced by
extractEndpointFromPath(url.pathname, TRANSPORT_PREFIXES); update any
imports/exports if needed to keep naming consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant